﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
using System.Threading;

namespace Iveonik.ConsoleWrapper
{
    /// <summary>
    /// Wrapper for the main cycle of console application. Is usefull for shielding exceptions, 
    /// parsing command-line parameters, mutexes processing, etc.
    /// </summary>
    public class AppWrapper
    {
        /// <summary>
        /// Usage example. It is printed to console if one of the following keys detected:
        /// /h /help /?
        /// </summary>
        public string UsageExample { get; set; }

        /// <summary>
        /// The names of mutexes for preventing of using same resources in different processes. If 
        /// any of the passed names is already exists, the application will be finished with code 
        /// <see cref="ExitCodes.MutexBusy"/> and the main action will not be run. All these mutexes 
        /// have the application lifescope, so they don't have the release operations. They all will 
        /// be released after the process releasing automatically by CLR. 
        /// 
        /// In reality generators run inside try/catch shield, so they can use dangerous
        /// operations.
        /// </summary>
        public IEnumerable<Func<string>> MutexNameGenerators { get; set; }

        /// <summary>
        /// List of possible parameters names. /h /help /? is always possible and added into list 
        /// automatically. To prevent the parameters checking, please leave the 
        /// <see cref="AdmissibleParameters"/> as null. If some inadmissible parameter detected, program
        /// will be terminated with <see cref="ExitCodes.InadmissibleParameter"/> error code. To allows 
        /// unnamed parameters, please add to list empty string ("").
        /// </summary>
        public IEnumerable<string> AdmissibleParameters { get; set; }

        /// <summary>
        /// List of combinations of mutually exclusive parameters. If some inadmissible combination 
        /// detected program be terminated with <see cref="ExitCodes.InadmissibleParameter"/> error code.
        /// </summary>
        public IEnumerable<IEnumerable<string>> MutuallyExclusiveParameters { get; set; }

        /// <summary>
        /// The event which are fired before the main action will run. It isn't called if help keys detected 
        /// or mutexes creation failed.
        /// </summary>
        public event EventHandler BeforeStart;

        /// <summary>
        /// The event which are fired after the main action has done. It isn't called if help keys detected 
        /// or mutexes creation failed.
        /// </summary>
        public event EventHandler<AfterFinishEventArgs> AfterFinish;

        /// <summary>
        /// The event which are fired after the main action has done. Handler of this event should write
        /// all the additional information into <see cref="SumUpEventArgs.SumUpInfo"/> property.
        /// </summary>
        public event EventHandler<SumUpEventArgs> SumUpRequest;

        /// <summary>
        /// The event are fired if some exception are unhandled in user code. Use this handler only if you need 
        /// in non-standard processing of exceptions. Standard processing is the logging error into trace end
        /// exiting application with non-zero exit code.
        /// </summary>
        public event EventHandler<UnhandledExceptionEventArgs> UnhandledException;

        /// <summary>
        /// String prefix added to a catched error message before Trace.TraceError calling. Can be useful for
        /// custom trace listeners. Example: "#diagnostic:critical:" or "#diagnostic:error".
        /// </summary>
        public string ErrorPrefix { set; get; }

        /// <summary>
        /// String prefix added to starting messages before Trace.TraceInformation calling. Can be useful for
        /// custom trace listeners. Example: "#diagnostic:event:started:".
        /// </summary>
        public string StartPrefix { set; get; }

        /// <summary>
        /// String prefix added to finishing messages before Trace.TraceInformation calling. Can be useful for
        /// custom trace listeners. Example: "#diagnostic:event:finished:"
        /// </summary>
        public string FinishPrefix { set; get; }

        /// <summary>
        /// If this property set to non-empty string and same named parameter found in the command-line,
        /// only error messages will be placed into trace output.
        /// </summary>
        public string SilentModeParameter { set; get; }

        /// <summary>
        /// Returns true if silent mode is enabled. See the <see cref="SilentModeParameter"/>.
        /// </summary>
        public bool SilentMode { get; internal set; }

        /// <summary>
        /// Syntactic sugar for short definition for MutexNamesGenerators setting in chain-style.
        /// </summary>
        /// <param name="mutexNameGenerators">New collection for <see cref="MutexNameGenerators"/> property.</param>
        /// <returns>Current instance.</returns>
        public AppWrapper SetMutexNameGenerators(params Func<string>[] mutexNameGenerators)
        {
            MutexNameGenerators = mutexNameGenerators;
            return this;
        }

        /// <summary>
        /// Syntactic sugar for definition of admissible parameters list in chain-style.
        /// </summary>
        /// <param name="admissibleParameters">New collection for <see cref="AdmissibleParameters"/> property.</param>
        /// <returns>Current instance.</returns>
        public AppWrapper SetAdmissibleParameters(params string[] admissibleParameters)
        {
            AdmissibleParameters = admissibleParameters;
            return this;
        }

        /// <summary>
        /// Syntactic sugar for definition of mutually exclusive parameters in chain-style.
        /// </summary>
        /// <param name="combinations">New collection for <see cref="AdmissibleParameters"/> property.</param>
        /// <returns>Current instance.</returns>
        public AppWrapper SetMutuallyExclusiveParameters(params string[][] combinations)
        {
            MutuallyExclusiveParameters = combinations;
            return this;
        }

        /// <summary>
        /// Syntactic sugar for definition of mutually exclusive parameters in chain-style.
        /// </summary>
        /// <param name="combination">Single set of mutually exclusive parameters (rule).</param>
        /// <returns>Current instance.</returns>
        public AppWrapper AddMutuallyExclusiveRule(params string[] combination)
        {
            var rules = new List<IEnumerable<string>>(1);
            rules.Add(combination);
            if (MutuallyExclusiveParameters == null)
            {
                MutuallyExclusiveParameters = rules;
            }
            else
            {
                MutuallyExclusiveParameters = MutuallyExclusiveParameters.Union(rules);
            }
            return this;
        }

        /// <summary>
        /// Syntactic sugar for definition of usage example in chain-style.
        /// </summary>
        /// <param name="usageExample">Usage example. It is printed to console if one of the following keys detected:
        /// /h /help /?</param>
        /// <returns>Current instance.</returns>
        public AppWrapper SetUsageExample(string usageExample)
        {
            UsageExample = usageExample;
            return this;
        }

        /// <summary>
        /// Syntactic sugar for setup silent-mode in chain-style.
        /// </summary>
        /// <param name="parameterName">Name of parameter which enabled the silent-mode. In this mode 
        /// <see cref="AppWrapper"/> does not perform informational trace. Only exceptions will be traced.</param>
        /// <returns>Current instance.</returns>
        public AppWrapper SetSilentModeParameter(string parameterName)
        {
            SilentModeParameter = parameterName;
            return this;
        }

        /// <summary>
        /// Syntactic sugar for adding <see cref="BeforeStart"/> handler in chain-style.
        /// </summary>
        /// <param name="handler">Event's handler.</param>
        /// <returns>Current instance.</returns>
        public AppWrapper AddBeforeStart(EventHandler handler)
        {
            BeforeStart += handler;
            return this;
        }

        /// <summary>
        /// Syntactic sugar for adding <see cref="AfterFinish"/> handler in chain-style.
        /// </summary>
        /// <param name="handler">Event's handler.</param>
        /// <returns>Current instance.</returns>
        public AppWrapper AddAfterFinish(EventHandler<AfterFinishEventArgs> handler)
        {
            AfterFinish += handler;
            return this;
        }

        /// <summary>
        /// Syntactic sugar for adding <see cref="SumUpRequest"/> handler in chain-style.
        /// </summary>
        /// <param name="handler">Event's handler.</param>
        /// <returns>Current instance.</returns>
        public AppWrapper AddSumUpRequest(EventHandler<SumUpEventArgs> handler)
        {
            SumUpRequest += handler;
            return this;
        }

        /// <summary>
        /// Syntactic sugar for adding <see cref="UnhandledException"/> handler in chain-style.
        /// </summary>
        /// <param name="handler">Event's handler.</param>
        /// <returns>Current instance.</returns>
        public AppWrapper AddUnhandledException(EventHandler<UnhandledExceptionEventArgs> handler)
        {
            UnhandledException += handler;
            return this;
        }

        private readonly Stack<Mutex> _mutexes = new Stack<Mutex>();

        /// <summary>
        /// The <see cref="Parameters"/> instance. It contains the parsed command line parameters.
        /// </summary>
        public Parameters Parameters
        {
            get { return _parameters; }
        }
        private readonly Parameters _parameters = new Parameters();

        private DateTime _startTime;
        private DateTime _finishTime;

        /// <summary>
        /// Method runs the <paramref name="action"/> and makes all associated service actions 
        /// like checking for the help keys, mutexes creation, errors shielding+logging, time 
        /// measurement. 
        /// </summary>
        /// <param name="action">Main operations of application.</param>
        public void Run(Action<AppWrapper> action)
        {
            if (!ShowUsage())
            {
                try
                {
                    CheckParameters();
                    MakeMutexes();
                    OnBeforeStart(new EventArgs());
                    action(this);
                }
                catch (Exception ex)
                {
                    UnhandledExceptionEventArgs e = new UnhandledExceptionEventArgs
                    {
                        ProcessAsError = true,
                        UnhandledException = ex,
                    };
                    OnUnhandledException(e);
                    if (e.ProcessAsError)
                    {
                        if (ex is ExitCodeException)
                            Environment.ExitCode = (ex as ExitCodeException).ExitCode;
                        else
                            Environment.ExitCode = ExitCodes.InternalError;
                        Trace.TraceError(WithPrefix(ErrorPrefix, "An error was occured. Error: {0}"), ex);
                    }
                }
                OnAfterFinish(new AfterFinishEventArgs());
            }
        }

        private void CheckParameters()
        {
            if (AdmissibleParameters != null)
            {
                Parameters.CheckForUnexpectedParameters(AdmissibleParameters);
            }
            Parameters.CheckForMutuallyExclusiveParameters(MutuallyExclusiveParameters);
            CheckSilentMode();
        }

        private void CheckSilentMode()
        {
            if (!string.IsNullOrEmpty(SilentModeParameter))
            {
                if (Parameters.HasParameter(SilentModeParameter))
                    SilentMode = Parameters.GetBooleanValue(SilentModeParameter, true);
            }
        }

        private void TraceInformation(string message)
        {
            if (!SilentMode)
                Trace.TraceInformation(message);
        }

        private static string WithPrefix(string prefix, string message)
        {
            if (!string.IsNullOrEmpty(prefix))
                return prefix + " " + (message ?? "");
            else
                return (message ?? "");
        }

        private void MakeMutexes()
        {
            if (MutexNameGenerators == null) // Nothing to do
                return;
            foreach (var mutexNameGenerator in MutexNameGenerators)
            {
                string mutexName = mutexNameGenerator();
                Mutex mutex = new Mutex(true, mutexName);
                _mutexes.Push(mutex);
                if (!mutex.WaitOne(0, true))
                    throw new ExitCodeException(ExitCodes.MutexBusy, "Mutex '" + mutexName + "' is already exists");
            }
        }

        private bool ShowUsage()
        {
            bool res = false;
            if (Parameters.HasParameter("?") || Parameters.HasParameter("h") || Parameters.HasParameter("help"))
            {
                Console.WriteLine(UsageExample);
                res = true;
            }
            return res;
        }

        /// <summary>
        /// Method runs before the main action running. Tryes to fire <see cref="BeforeStart"/> parameter 
        /// and starts the time measurement.
        /// </summary>
        /// <param name="e">Parameter for event handler.</param>
        protected virtual void OnBeforeStart(EventArgs e)
        {
            if (BeforeStart != null)
                BeforeStart(this, e);
            _startTime = DateTime.Now;
            TraceInformation(WithPrefix(StartPrefix, "Application started at " + _startTime));
        }

        /// <summary>
        /// Method runs after the main action running. Finishes the time measurement and
        /// tryes to fire <see cref="AfterFinish"/> parameter.
        /// </summary>
        /// <param name="e">Parameter for event handler.</param>
        protected virtual void OnAfterFinish(AfterFinishEventArgs e)
        {
            _finishTime = DateTime.Now;
            e.ProcessingDuration = _finishTime - _startTime;
            SumUpEventArgs sumUpArgs = new SumUpEventArgs();
            if (SumUpRequest != null)
                SumUpRequest(this, sumUpArgs);
            TraceInformation(WithPrefix(FinishPrefix, "Application finished at " + _finishTime +
                ". Duration = " + e.ProcessingDuration.TotalSeconds + " seconds." +
                " ExitCode = " + Environment.ExitCode +
                (String.IsNullOrEmpty(sumUpArgs.SumUpInfo) ? "" : ". Sum up info: " + sumUpArgs.SumUpInfo)));
            if (AfterFinish != null)
                AfterFinish(this, e);
        }

        /// <summary>
        /// Method runs if unhandled exception is throwed in processing code. It calls event's handlers sequence (if it exists).
        /// </summary>
        /// <param name="e">Event's argument for event's handlers.</param>
        protected virtual void OnUnhandledException(UnhandledExceptionEventArgs e)
        {
            if (UnhandledException != null)
                UnhandledException(this, e);
        }

        /// <summary>
        /// Arguments for sumup handler.
        /// </summary>
        public class SumUpEventArgs : EventArgs
        {
            /// <summary>
            /// Additional information should be added to the finishing record.
            /// </summary>
            public string SumUpInfo { get; set; }
        }

        /// <summary>
        /// Arguments for unhandled exception's handlers.
        /// </summary>
        public class UnhandledExceptionEventArgs : EventArgs
        {
            /// <summary>
            /// Exception was not handled by user's code.
            /// </summary>
            public Exception UnhandledException { get; set; }

            /// <summary>
            /// If true, the UnhundledException will be processed in standard way (message in Trace, 
            /// exit with non-zero code). Otherwise, application will be simple finished. 
            /// </summary>
            public bool ProcessAsError { get; set; }
        }

        /// <summary>
        /// Arguments for AfterFinish event handlers.
        /// </summary>
        public class AfterFinishEventArgs : EventArgs
        {
            /// <summary>
            /// Duration of the processing's cycle.
            /// </summary>
            public TimeSpan ProcessingDuration { get; protected internal set; }
        }
    }
}
